/**
 * 
 */
package com.feib.stms.security.web.access.intercept;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.security.web.util.AntUrlPathMatcher;
import org.springframework.security.web.util.UrlMatcher;

import com.feib.stms.model.MenuItem;
import com.feib.stms.model.Role;
import com.feib.stms.util.MenuLoader;

/** 
 * 
 * @title (#)SoeasyInvocationSecurityMetadataSourceService.java<br>
 * @description <br>
 * @author Jimmy Liu<br>
 * @version 1.0.0 2012/03/30
 * @copyright Far Eastern International Bank Copyright (c) 2010<br>
 * @2012/03/30 create by Jimmy Liu<br>
 *
 */
public class StmsFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

	private static final Set<String> HTTP_METHODS = new HashSet<String>(Arrays.asList("DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT", "TRACE"));

	protected final Log logger = LogFactory.getLog(getClass());

	//private Map<Object, List<ConfigAttribute>> requestMap = new LinkedHashMap<Object, List<ConfigAttribute>>();
	/** Stores request maps keyed by specific HTTP methods. A null key matches any method */
	private Map<String, Map<Object, Collection<ConfigAttribute>>> httpMethodMap =
		new HashMap<String, Map<Object, Collection<ConfigAttribute>>>();

	private UrlMatcher urlMatcher = new AntUrlPathMatcher();

	private boolean stripQueryStringFromUrls;

	@Resource(name = "menuLoader")
	private MenuLoader menuLoader;

	private String rolePrefix = "ROLE_";

	private Date reloadTime;

	private boolean reload = false;

	@PostConstruct
	public void loadSecurityMetadataSource() {
		
		httpMethodMap =	new HashMap<String, Map<Object, Collection<ConfigAttribute>>>();
		
		List<MenuItem> menuItems = menuLoader.getMenuItems();
		for (MenuItem menuItem : menuItems)
		{
			String action = menuItem.getActionPage();

			// 密碼變更，不列入控管
			if ("/Stms9001.action".equals(action))
				continue;

			if (StringUtils.isNotBlank(action)) 
			{
				Collection<ConfigAttribute> attrs = lookupAttributes(action, null);
				if (null == attrs)
					attrs = new ArrayList<ConfigAttribute>();

				for (Role role : menuItem.getRoles())
				{
					SecurityConfig sc = new SecurityConfig(rolePrefix + role.getRolePk()+ "_" + role.getRoleNo() + "_" + role.getRoleName()); // 格式請對應 StmsUserDetailsWrapper
					if (! attrs.contains(sc))
						attrs.add(sc);
				}

				addSecureUrl(action, null, attrs);
			}		
		}
		reloadTime = new Date(); 
	}

	/**
	 * 
	 * Adds a URL,attribute-list pair to the request map, first allowing the <tt>UrlMatcher</tt> to
	 * process the pattern if required, using its <tt>compile</tt> method. The returned object will be used as the key
	 * to the request map and will be passed back to the <tt>UrlMatcher</tt> when iterating through the map to find
	 * a match for a particular URL.
	 * @param pattern URK網址
	 * @param method POST,GET....
	 * @param attrs 此pattern(URL網址)所適用的角色
	 */
	private void addSecureUrl(String pattern, String method, Collection<ConfigAttribute> attrs) {

		Map<Object, Collection<ConfigAttribute>> mapToUse = getRequestMapForHttpMethod(method);

		mapToUse.put(urlMatcher.compile(pattern), attrs);

		if (logger.isDebugEnabled()) {
			logger.debug("Added URL pattern: " + pattern + "; attributes: " + attrs +
					(method == null ? "" : " for HTTP method '" + method + "'"));
		}
	}


	/**
	 * 取得所有的的角色
	 */
	public Collection<ConfigAttribute> getAllConfigAttributes() {

		Set<ConfigAttribute> allAttributes = new HashSet<ConfigAttribute>();

		for (Map.Entry<String, Map<Object, Collection<ConfigAttribute>>> entry : httpMethodMap.entrySet()) {
			for (Collection<ConfigAttribute> attrs : entry.getValue().values()) {
				allAttributes.addAll(attrs);
			}
		}

		return allAttributes;
	}

	/**
	 * 取得傳入的URL網址所適用的角色.
	 * @param object FilterInvocation，可以從中取得URK網址
	 */
	public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
		if ((object == null) || !this.supports(object.getClass())) {
			throw new IllegalArgumentException("Object must be a FilterInvocation");
		}


		if (reload)
		{
			// 設定 5 秒後 reload ，避免前一個transation 尚未commit時就抓取，會抓不到
			Calendar cal = Calendar.getInstance();
			cal.add(Calendar.HOUR, -4); // 搭配後面每四小時更新一次
			cal.add(Calendar.SECOND, -5);
			reloadTime = cal.getTime();
			reload = false;
		}
		else
		{
			Calendar cal = Calendar.getInstance();
			cal.setTime(reloadTime);
			cal.add(Calendar.HOUR, 4); //4 小時更新一次

			if (new Date().after(reloadTime))
			{
				loadSecurityMetadataSource();
				reloadTime = cal.getTime();
			}
		}



		String url = ((FilterInvocation) object).getRequestUrl();
		String method = ((FilterInvocation) object).getHttpRequest().getMethod();

		return lookupAttributes(url, method);
	}

	public boolean supports(Class<?> clazz) {
		return FilterInvocation.class.isAssignableFrom(clazz);
	}

	/**
	 * Performs the actual lookup of the relevant <tt>ConfigAttribute</tt>s for the given <code>FilterInvocation</code>.
	 * <p>
	 * By default, iterates through the stored URL map and calls the
	 * {@link UrlMatcher#pathMatchesUrl(Object path, String url)} method until a match is found.
	 *
	 * @param url the URI to retrieve configuration attributes for
	 * @param method the HTTP method (GET, POST, DELETE...), or null for any method.
	 *
	 * @return the <code>ConfigAttribute</code>s that apply to the specified <code>FilterInvocation</code>
	 * or null if no match is found
	 */
	public final Collection<ConfigAttribute> lookupAttributes(String url, String method) {
		if (stripQueryStringFromUrls) {
			// 去除問候後面的參數
			// Strip anything after a question mark symbol, as per SEC-161. See also SEC-321
			int firstQuestionMarkIndex = url.indexOf("?");

			if (firstQuestionMarkIndex != -1) {
				url = url.substring(0, firstQuestionMarkIndex);
			}
		}

		if (urlMatcher.requiresLowerCaseUrl()) {
			url = url.toLowerCase();

			if (logger.isDebugEnabled()) {
				logger.debug("Converted URL to lowercase, from: '" + url + "'; to: '" + url + "'");
			}
		}

		// Obtain the map of request patterns to attributes for this method and lookup the url.
		Collection<ConfigAttribute> attributes = extractMatchingAttributes(url, httpMethodMap.get(method));

		// If no attributes found in method-specific map, use the general one stored under the null key
		if (attributes == null) {
			attributes = extractMatchingAttributes(url, httpMethodMap.get(null));
		}

		return attributes;
	}

	private Collection<ConfigAttribute> extractMatchingAttributes(String url, Map<Object, Collection<ConfigAttribute>> map) {
		if (map == null) {
			return null;
		}

		final boolean debug = logger.isDebugEnabled();

		for (Map.Entry<Object, Collection<ConfigAttribute>> entry : map.entrySet()) {
			Object p = entry.getKey();
			boolean matched = urlMatcher.pathMatchesUrl(entry.getKey(), url);

			if (debug) {
				logger.debug("Candidate is: '" + url + "'; pattern is " + p + "; matched=" + matched);
			}

			if (matched) {
				return entry.getValue();
			}
		}
		return null;
	}

	/**
	 * Return the HTTP method specific request map, creating it if it doesn't already exist.
	 * @param method GET, POST etc
	 * @return map of URL patterns to <tt>ConfigAttribute</tt>s for this method.
	 */
	private Map<Object, Collection<ConfigAttribute>> getRequestMapForHttpMethod(String method) {
		if (method != null && !HTTP_METHODS.contains(method)) {
			throw new IllegalArgumentException("Unrecognised HTTP method: '" + method + "'");
		}

		Map<Object, Collection<ConfigAttribute>> methodRequestMap = httpMethodMap.get(method);

		if (methodRequestMap == null) {
			methodRequestMap = new LinkedHashMap<Object, Collection<ConfigAttribute>>();
			httpMethodMap.put(method, methodRequestMap);
		}

		return methodRequestMap;
	}

	public void setMenuLoader(MenuLoader menuLoader) {
		this.menuLoader = menuLoader;
	}

	public void setToReload()
	{
		this.reload = true;
	}


}
